#include <stdio.h>

// 错误分类：1.编译错误，2.链接错误，3.运行错误

// const 的用法举例
// int main()
// {
//     int a = 10, b = 20;
//     const int *p1 = &a;
//     // *p1 = b;
//     // error。const 修饰 *p1 表示 *p1 不能改变
//     // （即，不能改变 a）
//     // 但可以指向别的变量
//
//     int *const p2 = &a;
//     // p2 = &b;
//     // error，const 修饰 p2 表示 p2 不能改变
//     // （即，只能指向 a），
//     // 但可以改变 a
//
//     const int *const p3 = &a;
//     // 既不能通过 *p 改变 a，也不能改变 p（即，指向别的变量）
//     return 0;
// }

/*
 * 那么如何写出易于调试的代码呢
 *
 * 优秀的代码风格：效率高，可读性高，可维护性高，可拓展性高
 *
 * 1.使用 assert（断言，assert.h 库）
 * 2.尽量使用 const
 * 3.平常写代码时保持严谨，该空格空格，该缩进缩进，该精简精简
 * 4.加必要的注释
 * 5.多见识隐晦的 bug
 */

// 示例：手搓 strcpy
//
// 版本一（5分，还是我自己写的，呜呜）
// void my_strcpy(char *dest, char *scr)
// {
//     while (*scr != '\0')
//     {
//         *dest = *scr;
//         dest++;
//         scr++;
//     }
//     *dest = '\0';
// }
// 版本二（6分，精炼）
// void my_strcpy(char *dest, char *scr)
// {
//     while (*dest++ = *scr++)
//         ;
// }
// 版本三（7分，安全）
// void my_strcpy(char *dest, char *scr)
// {
//     if (dest && scr)
//         while (*dest++ = *scr++)
//             ;
// }
// 版本四（8分，有警报）
// #include <assert.h>
// void my_strcpy(char *dest, char *scr)
// {
//     assert(dest != NULL);
//     assert(scr != NULL);
//     // 断言。() 里面为真则无事，为假则警报
//     while (*dest++ = *scr++)
//         ;
// }
// 版本五（10分，就是库函数的写法）
// #include <assert.h>
// char *my_strcpy(char *dest, const char *scr)
// {
//     // 两处改动。
//     // 1.返回复制好的字符串。增加了函数的功能。
//     // 2.用 const 保护 *scr 不被修改。更谨慎。
//     assert(dest != NULL);
//     assert(scr != NULL);
//     char *ret = dest;
//     while (*dest++ = *scr++)
//         ;
//     return ret;
// }

// int main()
// {
//     int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//     int i = 0;
//     for (i = 0; i <= 11; i++)
//     {
//         printf("hh ");
//         arr[i] = 0;
//     }
//     return 0;
// }
// 在 debug 模式下死循环，在 release 模式下不死循环
// 笔试题，出自《C陷阱和缺陷》
//
// 先说为什么死循环
// 1.栈区的默认使用：先使用高地址的空间，再使用低地址的空间
// 2.随着数组下标增长，地址由低到高变化
//
// 循环变量 i 声明在数组前面，为高地址；
// 数组下标的增长让数组能够越界访问到 i（只要越界到一定长度即可）
// 不同编译器下 i 和 arr 的距离不同，需要越界的长度不同
// 解决办法：把 i 声明在 arr 后？（编译器有可能会自动调整声明顺序）
// 其实，不要让数组越界才是硬道理
//
// 再来看为什么不死循环
// 试着分别在 debug 和 release 下看看 i 和 arr 的地址就能明白
// release 优化了很多地方，包括代码在内存里的分布和结构。
// 因此，
// 程序员在 debug 下测试好的代码，有可能被测试员在 release 下测出 bug
// 对于程序员，要承认自己的代码不是最完美的；
// 对于测试员，发现 bug 要及时保留证据

// 调试技巧
// 什么是 bug
// 什么是调试及重要性
// 调试的步骤
//
// 发现问题（程序员-->测试员-->用户）
// 找出问题（隔离，消除等方法）
// 分析问题
// 解决问题
// 验证结果
//
// debug 和 release
//
// debug，即调试版本，包含调试信息，且不作任何优化，便于程序员调试；
// release，即发布版本，在大小和运行速度上最优化，便于用户使用。
// release 不能调试
//
// 二者在内存大小和功能实现上都有差别
// debug 包含调试信息，占内存大；
// 而 release 进行优化，可有限度地修正代码。
//
// 断点：启动调试时，程序会直接执行到断点处。
// 注意，是按逻辑顺序执行。
// 如果断点在循环体内则会循环执行，
// 表面上看程序停留在循环体的断点处，实际上已经执行了一次循环。
//
// 断点可以设置条件。
//
// 调试时查看程序当前信息
// （临时变量，内存信息，调用堆栈，汇编信息，寄存器信息等等）
//
// 复杂调试场景：多线程程序调试
